package org.dromara.commission.controller;

import java.io.*;
import java.nio.charset.StandardCharsets;
import java.nio.file.Path;
import java.util.*;
import java.util.concurrent.ScheduledExecutorService;
import java.util.concurrent.TimeUnit;
import java.util.concurrent.atomic.AtomicBoolean;
import java.util.concurrent.atomic.AtomicInteger;
import java.util.concurrent.atomic.AtomicLong;
import java.util.concurrent.locks.Lock;
import java.util.concurrent.locks.ReentrantLock;

import cn.hutool.core.bean.BeanUtil;
import lombok.RequiredArgsConstructor;
import jakarta.servlet.http.HttpServletResponse;
import jakarta.validation.constraints.*;
import cn.dev33.satoken.annotation.SaCheckPermission;
import org.dromara.commission.domain.vo.*;
import org.dromara.commission.listener.*;
import org.dromara.commission.listener.redisPlus.RedisIndexTable;
import org.dromara.commission.listener.redisPlus.RedisPlusUtil;
import org.dromara.common.core.constant.GlobalConstants;
import org.dromara.common.core.utils.StringUtils;
import org.dromara.common.core.utils.file.FileUtils;
import org.dromara.common.oss.core.OssClient;
import org.dromara.common.oss.entity.UploadResult;
import org.dromara.common.oss.factory.OssFactory;
import org.dromara.common.redis.utils.RedisUtils;
import org.dromara.common.satoken.utils.LoginHelper;
import org.dromara.common.websocket.dto.WebSocketMessageDto;
import org.dromara.common.websocket.utils.WebSocketUtils;
import org.springframework.http.MediaType;
import org.springframework.web.bind.annotation.*;
import org.springframework.validation.annotation.Validated;
import org.dromara.common.idempotent.annotation.RepeatSubmit;
import org.dromara.common.log.annotation.Log;
import org.dromara.common.web.core.BaseController;
import org.dromara.common.mybatis.core.page.PageQuery;
import org.dromara.common.core.domain.R;
import org.dromara.common.core.validate.AddGroup;
import org.dromara.common.core.validate.EditGroup;
import org.dromara.common.log.enums.BusinessType;
import org.dromara.common.excel.utils.ExcelUtil;
import org.dromara.commission.domain.bo.LzshCommissionListBo;
import org.dromara.commission.service.ILzshCommissionListService;
import org.dromara.common.mybatis.core.page.TableDataInfo;
import org.springframework.web.multipart.MultipartFile;

/**
 * 佣金清单
 *
 * @author 小将lcz
 * @date 2024-07-25
 */
@Validated
@RequiredArgsConstructor
@RestController
@RequestMapping("/commission/commissionList")
public class LzshCommissionListController extends BaseController {

    private final ILzshCommissionListService lzshCommissionListService;

    private final ScheduledExecutorService scheduledExecutorService;

    private final String flagMysql = "mysql";

    private final String flagRedis = "redis";
    /**
     * 查询佣金清单列表
     */
    @SaCheckPermission("commission:commissionList:list")
    @GetMapping("/list")
    public TableDataInfo<LzshCommissionListVo> list(LzshCommissionListBo bo, PageQuery pageQuery) {
        TableDataInfo<LzshCommissionListVo> lzshCommissionListVoTableDataInfo = new TableDataInfo<>();
        //获取redis索引表
        RedisIndexTable redisIndexTable = RedisPlusUtil.initIndexTable(LzshCommissionListImportListener.class);
        //如果索引表为空，说明数据已经完全同步入库，就去数据库查数据
        if(redisIndexTable.getCount() <= 0){
            //如果索引表为空，说明数据已经完全同步入库，就去数据库查数据
            //如果不是超级管理员就只查找自己的
            if(!LoginHelper.isSuperAdmin()){
                String username = LoginHelper.getUsername();//获取商家的经营主体码businessCode
                bo.setCol09(username);
            }
            return lzshCommissionListService.queryPageListByCol09(bo, pageQuery);//不能调用
        }
        //存在redis索引表。去redis缓存查数据
        RedisIndexTable.Window window = new RedisIndexTable.Window();
        Integer pageForm = (pageQuery.getPageNum()-1)*pageQuery.getPageSize();//查询窗口第一条记录的位序
        //如果不是超级管理员就只查找自己的
        if(!LoginHelper.isSuperAdmin()){
            //根据username拼接redis的key前缀头keyHead
            String userKey = LoginHelper.getUsername();//获取商家的经营主体码businessCode
            //根据 pageNum 和 pageSize 以及 keyHead 来获取查询窗口（item列表以及首key偏移量）
            window = redisIndexTable.getWindowByKeyHead(userKey, pageForm, pageQuery.getPageSize());
        }
        else{
            //根据 pageNum 和 pageSize 来获取查询窗口（item列表以及首key偏移量）
            window = redisIndexTable.getWindow(pageForm, pageQuery.getPageSize());
        }
        //调用redisPlusUtil来通过查询窗口获取list
        lzshCommissionListVoTableDataInfo.setRows(RedisPlusUtil.getCacheListByWindow(window));
        lzshCommissionListVoTableDataInfo.setTotal(window.getTotal());
        lzshCommissionListVoTableDataInfo.setMsg("查询成功");
        lzshCommissionListVoTableDataInfo.setCode(200);
        return lzshCommissionListVoTableDataInfo;
    }

    /**
     * 关闭下载的webSocket连接操作
     */

    @GetMapping("/closeDownload")
    public void closeDownload() {
        WebSocketMessageDto dto = new WebSocketMessageDto();
        dto.setSessionKeys(List.of(LoginHelper.getUserId()));//设置客户端id,获取 WebSocket 链接
        //设置为可删除
        RedisUtils.setCacheObject(GlobalConstants.GLOBAL_REDIS_KEY+"export:" + LoginHelper.getUsername(), "false");
        //设置不刷新了
        dto.setMessage("exportUrl$" + "*" + "$" + -1);
        System.out.println("调用关闭连接的方法");
        WebSocketUtils.publishMessage(dto);
    }

    /**
     * 导出佣金清单列表
     */
    @SaCheckPermission("commission:commissionList:export")
    @Log(title = "佣金清单", businessType = BusinessType.EXPORT)
    @PostMapping("/export")
    public R<Void> export(LzshCommissionListBo bo, HttpServletResponse response) {
        //获取userId
        Long userId = LoginHelper.getUserId();
        //获取租户
        String username = LoginHelper.getUsername();
        boolean superAdmin = LoginHelper.isSuperAdmin(userId);
        //在此处开启线程返回结果，避免提示连接超时，任务交给websocket执行
        scheduledExecutorService.execute(() -> {
            //获取redis索引表
            RedisIndexTable redisIndexTable = RedisPlusUtil.initIndexTable(LzshCommissionListImportListener.class);
            //如果索引表为空，redis不存在数据，就去数据库查数据
            if (redisIndexTable.getCount() <= 0) {
                //导出佣金清单的数据为excel
                try {
                    exportCommissionExcel(bo, flagMysql,userId,username,superAdmin);
                } catch (Exception e) {
                    throw new RuntimeException(e);
                } finally {
                    //无论是否下载成功最终都会删除,删除redis的标记
                    RedisUtils.deleteObject(GlobalConstants.GLOBAL_REDIS_KEY+"export:" + username);
                }
            } else { //否则去查询redis的
                try {
                    exportCommissionExcel(bo, flagRedis,userId,username,superAdmin);
                } catch (Exception e) {
                    throw new RuntimeException(e);
                } finally {
                    //删除redis的标记
                    RedisUtils.deleteObject(GlobalConstants.GLOBAL_REDIS_KEY+"export:" + username);
                }
            }
        });
        return R.ok();
    }

    /**
     * 私有方法作为去导出操作
     * @param bo 查询参数
     * @param flag redis或者mysql
     * @param userId 用户id
     * @param username 用户名
     * @param superAdmin 是否是超级管理员
     * @throws IOException
     * @throws InterruptedException
     */
    private void exportCommissionExcel(LzshCommissionListBo bo, String flag,Long userId,String username,boolean superAdmin) throws IOException, InterruptedException {
        List<LzshCommissionListVo> list = new ArrayList<>();
        WebSocketMessageDto dto = new WebSocketMessageDto();
        dto.setSessionKeys(List.of(userId));//设置客户端id,获取 WebSocket 链接
        //获取redis索引表
        RedisIndexTable redisIndexTable = RedisPlusUtil.initIndexTable(LzshCommissionListImportListener.class);
        //设置当前导出用户的标记，正在导出则不可再操作导出，已导出了才能进行下一次的导出，防止多次下载
        if (RedisUtils.getCacheObject(GlobalConstants.GLOBAL_REDIS_KEY+"export:" + username) == null || RedisUtils.getCacheObject((GlobalConstants.GLOBAL_REDIS_KEY+"export:" + username)) == "false") {
            //如果不是超级管理员就只查找自己的
            if (!superAdmin) {
                bo.setCol09(username);
            }
            //加一个正在上传的标志，如果用户刷新还没下载的话以这个标志去提示正在下载数据中,0表示还没有下载
            dto.setMessage("loading$" + 0 + "$" + "佣金清单");
            WebSocketUtils.publishMessage(dto);
            //加入判断的导出标记，防止重复导出
            RedisUtils.setCacheObject(GlobalConstants.GLOBAL_REDIS_KEY+"export:" + username, "true");
            //数据库里查询数据
            if ("mysql".equals(flag))
                list = lzshCommissionListService.queryList(bo);
            else if ("redis".equals(flag)) { //调用redis查询数据
                RedisIndexTable.Window window;
                //如果不是超级管理员就只查找自己的
                if (!superAdmin) {
                    //根据 pageNum 和 pageSize 以及 keyHead 来获取查询窗口（item列表以及首key偏移量）
                    window = redisIndexTable.getWindowByKeyHead(username);
                } else {
                    //根据 pageNum 和 pageSize 来获取查询窗口（item列表以及首key偏移量）
                    window = redisIndexTable.getWindow(0, Integer.MAX_VALUE);
                }
                //调用redisPlusUtil来通过查询窗口获取list
                list = RedisPlusUtil.getCacheListByWindow(window);
            }
            // 指定临时文件路径
            File file = new File("output");
            //如果不存在output则创建这个目录
            if(!file.exists()){
                //创建这个目录
                file.mkdirs();
            }
            //拼接这个路径
            String filePath = file.getPath() + "/" + username + "佣金清单.xlsx";
            File path = new File(filePath);
            if (!path.exists()) {
                FileOutputStream op = new FileOutputStream(filePath);
                //将数据写入到excel中
                ExcelUtil.exportExcel(list, "佣金清单", LzshCommissionListVo.class, op);
                // 关闭输出流
                op.close();
            }
            //文件后缀
            String suffix = StringUtils.substring(filePath, filePath.lastIndexOf("."), filePath.length());
            //创建OSS实例,去缓存读取默认的配置（前台设置好的且开启的）获得OSS上传资格
            OssClient ossClient = OssFactory.instance();
            //上传文件到OSS
            UploadResult upload = ossClient.upload(Path.of(filePath), ossClient.getPath("", suffix), null, FileUtils.getMimeType(filePath));
            //获得了url地址，使用webSocket一直通知前端，直到前端完成下载后，删除redis的标记，并删除oss上的文件
            while (true) {//websocket可能会断开,如果没有成功返回下载请求使用循环每10秒请求一次
                if (RedisUtils.getCacheObject(GlobalConstants.GLOBAL_REDIS_KEY+"export:" + username).equals("false")) {
                    //删除redis的标记
                    RedisUtils.deleteObject(GlobalConstants.GLOBAL_REDIS_KEY+"export:" + username);
                    //删除oss上的文件
                    ossClient.delete(upload.getUrl());
                    System.out.println("请求已结束");
                    break;
                } else {
                    dto.setMessage("exportUrl$" + upload.getUrl() + "$" + 1);
                    WebSocketUtils.publishMessage(dto);
                    System.out.println("正在发送请求");
                }
                Thread.sleep(10000);
            }
        }
    }


    /**
     * 获取导入模板
     */
    @PostMapping("/importTemplate")
    public void importTemplate(HttpServletResponse response) {
        ExcelUtil.exportExcel(new ArrayList<>(), "佣金清单数据", LzshCommissionListVo.class, response);
    }

    /**
     * 导入数据
     *
     * @param file          导入文件
     * @param isClear 是否更新已存在数据
     */
    @RepeatSubmit(interval = 10000,message = "不能重复提交")
    @Log(title = "佣金清单管理", businessType = BusinessType.IMPORT)
    @SaCheckPermission("commission:lzshCommissionList:import")
    @PostMapping(value = "/importData", consumes = MediaType.MULTIPART_FORM_DATA_VALUE)
    public R<Void> importData(@RequestPart("file") MultipartFile file, @RequestParam("updateSupport") boolean isClear) throws Exception {
        WebSocketMessageDto dto = new WebSocketMessageDto();
        dto.setSessionKeys(List.of(LoginHelper.getUserId()));//设置客户端id,获取 WebSocket 链接

        final AtomicBoolean isWriteEnd = new AtomicBoolean(false);
        final AtomicInteger insertCount = new AtomicInteger(0); // 初始值为0

        //获取csv文件的总记录数
        BufferedReader reader = new BufferedReader(new InputStreamReader(file.getInputStream()));
        int lines = 0;
        while (reader.readLine() != null) {
            lines++;
        }
        reader.close();


        LzshCommissionListImportListener importListener = new LzshCommissionListImportListener();
        if(isClear){
            importListener.clear();
        }

        //开启读取进度监听
        int finalLines = lines - 1;//去掉表头行
        scheduledExecutorService.schedule(() -> {
            int readNum = importListener.getReadNum();
            while (readNum < finalLines){
                int per = (int) Math.round(readNum / (finalLines * 1.0) * 100); // 变成百分比的向下取整
                dto.setMessage("longConnection:$title{正在导入中...请耐心等待}$content{" + per + "}$type{message}");
                WebSocketUtils.publishMessage(dto);
                try {
                    Thread.sleep(1000);
                } catch (InterruptedException e) {
                    throw new RuntimeException(e);
                }
                readNum = importListener.getReadNum();
            }
            while(!importListener.getRedisPushEnd()){
                dto.setMessage("longConnection:$title{正在导入中...请耐心等待}$content{99}$type{message}");
                WebSocketUtils.publishMessage(dto);
                try {
                    Thread.sleep(1000);
                } catch (InterruptedException e) {
                    throw new RuntimeException(e);
                }
            }
            dto.setMessage("shutDownLongConnection:$title{导入完毕！！！}$content{100}$type{message}");
            WebSocketUtils.publishMessage(dto);
        }, 0, TimeUnit.SECONDS);

        ExcelUtil.importExcel(file.getInputStream(), LzshCommissionListImportVo.class, importListener);//开始读取导入
        try {
            Thread.sleep(2000);
        } catch (InterruptedException e) {
            throw new RuntimeException(e);
        }
        //文件解析成功
        HashMap<String, List<LzshCommissionListImportVo>> resultData = importListener.getResultData();
        // 创建一个空的列表，用于存放所有的 LzshCommissionListVo 对象
        List<LzshCommissionListImportVo> resultList = new ArrayList<>();
        // 遍历HashMap的所有value，将每个List<LzshCommissionListVo>的元素加入resultList
        for (List<LzshCommissionListImportVo> list : resultData.values()) {
            for(LzshCommissionListImportVo t : list){
                resultList.add(t);
            }
        }

        if(!lzshCommissionListService.clear()){
            return R.ok("写入异常！！！");//删表建表失败
        }

        scheduledExecutorService.schedule(() -> {
//            int lastCount = -1;
            while (!isWriteEnd.get()){
//                Long count = lzshCommissionListService.queryCount(new LzshCommissionListBo());
                int count = insertCount.get();
//                if(count == lastCount){
//                    isWriteEnd.set(true);
//                    dto.setMessage("shutDownLongConnection:$title{后台写入异常！}$content{100}$type{notification}");
//                }
                if(count == resultList.size()){
                    isWriteEnd.set(true);
                    dto.setMessage("shutDownLongConnection:$title{写入完毕！}$content{100}$type{notification}");
                    //所有都插入成功，创建索引,清空redis缓存
                    lzshCommissionListService.creatIndex();//创建索引
                    RedisIndexTable redisIndexTable = RedisPlusUtil.initIndexTable(LzshCommissionListImportListener.class);
                    RedisPlusUtil.clear(redisIndexTable);//清空redis
                }else{
                    int per = (int) Math.round(count / (resultList.size() * 1.0) * 100); // 变成百分比的向下取整
                    dto.setMessage("longConnection:$title{后台写入进度：}$content{" + per + "}$type{notification}");
                }
                WebSocketUtils.publishMessage(dto);
//                lastCount = Math.toIntExact(count);
                try {
                    Thread.sleep(2000);
                } catch (InterruptedException e) {
                    throw new RuntimeException(e);
                }
            }
        }, 0, TimeUnit.SECONDS);

        //分批次开启多线程插入数据库
        for(int i=0;i<resultList.size();i+=5000){
            final int startIndex = i;  // 使用 final 变量来确保在lambda表达式中捕获的是期望的值
            scheduledExecutorService.schedule(() -> {
                int endIndex = Math.min(startIndex+5000, resultList.size());
                lzshCommissionListService.insertBacth(resultList.subList(startIndex,endIndex));
                insertCount.addAndGet(endIndex-startIndex);
            }, 0, TimeUnit.SECONDS);
        }

        return R.ok(importListener.getAnalysis());
    }

    /**
     * 获取佣金清单详细信息
     *
     * @param id 主键
     */
    @SaCheckPermission("commission:commissionList:query")
    @GetMapping("/{id}")
    public R<LzshCommissionListVo> getInfo(@NotNull(message = "主键不能为空") @PathVariable Long id) {
        return R.ok(lzshCommissionListService.queryById(id));
    }

    /**
     * 新增佣金清单
     */
    @SaCheckPermission("commission:commissionList:add")
    @Log(title = "佣金清单", businessType = BusinessType.INSERT)
    @RepeatSubmit()
    @PostMapping()
    public R<Void> add(@Validated(AddGroup.class) @RequestBody LzshCommissionListBo bo) {
        return toAjax(lzshCommissionListService.insertByBo(bo));
    }

    /**
     * 修改佣金清单
     */
    @SaCheckPermission("commission:commissionList:edit")
    @Log(title = "佣金清单", businessType = BusinessType.UPDATE)
    @RepeatSubmit()
    @PutMapping()
    public R<Void> edit(@Validated(EditGroup.class) @RequestBody LzshCommissionListBo bo) {
        return toAjax(lzshCommissionListService.updateByBo(bo));
    }

    /**
     * 删除佣金清单
     *
     * @param ids 主键串
     */
    @SaCheckPermission("commission:commissionList:remove")
    @Log(title = "佣金清单", businessType = BusinessType.DELETE)
    @DeleteMapping("/{ids}")
    public R<Void> remove(@NotEmpty(message = "主键不能为空")
                          @PathVariable Long[] ids) {
        return toAjax(lzshCommissionListService.deleteWithValidByIds(List.of(ids), true));
    }
}
